{
 "cells": [
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#| hide\n",
    "#| eval: false\n",
    "! [ -e /content ] && pip install -Uqq fastai  # upgrade fastai on colab"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#| default_exp data.transforms"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "from __future__ import annotations\n",
    "from fastai.torch_basics import *\n",
    "from fastai.data.core import *\n",
    "from fastai.data.load import *\n",
    "from fastai.data.external import *\n",
    "\n",
    "from sklearn.model_selection import train_test_split\n",
    "\n",
    "import posixpath"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#| hide\n",
    "from nbdev.showdoc import *"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Data transformations\n",
    "\n",
    "> Functions for getting, splitting, and labeling data, as well as generic transforms"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Get, split, and label"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "For most data source creation we need functions to get a list of items, split them in to train/valid sets, and label them. fastai provides functions to make each of these steps easy (especially when combined with `fastai.data.blocks`)."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Get"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "First we'll look at functions that *get* a list of items (generally file names).\n",
    "\n",
    "We'll use *tiny MNIST* (a subset of MNIST with just two classes, `7`s and `3`s) for our examples/tests throughout this page."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/html": [
       "\n",
       "<style>\n",
       "    /* Turns off some styling */\n",
       "    progress {\n",
       "        /* gets rid of default border in Firefox and Opera. */\n",
       "        border: none;\n",
       "        /* Needs to be in here for Safari polyfill so background images work as expected. */\n",
       "        background-size: auto;\n",
       "    }\n",
       "    progress:not([value]), progress:not([value])::-webkit-progress-bar {\n",
       "        background: repeating-linear-gradient(45deg, #7e7e7e, #7e7e7e 10px, #5c5c5c 10px, #5c5c5c 20px);\n",
       "    }\n",
       "    .progress-bar-interrupted, .progress-bar-interrupted::-webkit-progress-bar {\n",
       "        background: #F44336;\n",
       "    }\n",
       "</style>\n"
      ],
      "text/plain": [
       "<IPython.core.display.HTML object>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "text/html": [
       "\n",
       "    <div>\n",
       "      <progress value='344064' class='' max='342207' style='width:300px; height:20px; vertical-align: middle;'></progress>\n",
       "      100.54% [344064/342207 00:00&lt;00:00]\n",
       "    </div>\n",
       "    "
      ],
      "text/plain": [
       "<IPython.core.display.HTML object>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "text/plain": [
       "(#2) [Path('/Users/jhoward/.fastai/data/mnist_tiny/train/7'),Path('/Users/jhoward/.fastai/data/mnist_tiny/train/3')]"
      ]
     },
     "execution_count": null,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "path = untar_data(URLs.MNIST_TINY)\n",
    "(path/'train').ls()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "def _get_files(p, fs, extensions=None):\n",
    "    p = Path(p)\n",
    "    res = [p/f for f in fs if not f.startswith('.')\n",
    "           and ((not extensions) or f'.{f.split(\".\")[-1].lower()}' in extensions)]\n",
    "    return res"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "def get_files(path, extensions=None, recurse=True, folders=None, followlinks=True):\n",
    "    \"Get all the files in `path` with optional `extensions`, optionally with `recurse`, only in `folders`, if specified.\"\n",
    "    path = Path(path)\n",
    "    folders=L(folders)\n",
    "    extensions = setify(extensions)\n",
    "    extensions = {e.lower() for e in extensions}\n",
    "    if recurse:\n",
    "        res = []\n",
    "        for i,(p,d,f) in enumerate(os.walk(path, followlinks=followlinks)): # returns (dirpath, dirnames, filenames)\n",
    "            if len(folders) !=0 and i==0: d[:] = [o for o in d if o in folders]\n",
    "            else:                         d[:] = [o for o in d if not o.startswith('.')]\n",
    "            if len(folders) !=0 and i==0 and '.' not in folders: continue\n",
    "            res += _get_files(p, f, extensions)\n",
    "    else:\n",
    "        f = [o.name for o in os.scandir(path) if o.is_file()]\n",
    "        res = _get_files(path, f, extensions)\n",
    "    return L(res)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "This is the most general way to grab a bunch of file names from disk. If you pass `extensions` (including the `.`) then returned file names are filtered by that list. Only those files directly in `path` are included, unless you pass `recurse`, in which case all child folders are also searched recursively. `folders` is an optional list of directories to limit the search to."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "(#709) [Path('/Users/jhoward/.fastai/data/mnist_tiny/train/7/9243.png'),Path('/Users/jhoward/.fastai/data/mnist_tiny/train/7/9519.png'),Path('/Users/jhoward/.fastai/data/mnist_tiny/train/7/7534.png'),Path('/Users/jhoward/.fastai/data/mnist_tiny/train/7/9082.png'),Path('/Users/jhoward/.fastai/data/mnist_tiny/train/7/8377.png'),Path('/Users/jhoward/.fastai/data/mnist_tiny/train/7/994.png'),Path('/Users/jhoward/.fastai/data/mnist_tiny/train/7/8559.png'),Path('/Users/jhoward/.fastai/data/mnist_tiny/train/7/8217.png'),Path('/Users/jhoward/.fastai/data/mnist_tiny/train/7/8571.png'),Path('/Users/jhoward/.fastai/data/mnist_tiny/train/7/8954.png')...]"
      ]
     },
     "execution_count": null,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "t3 = get_files(path/'train'/'3', extensions='.png', recurse=False)\n",
    "t7 = get_files(path/'train'/'7', extensions='.png', recurse=False)\n",
    "t  = get_files(path/'train', extensions='.png', recurse=True)\n",
    "test_eq(len(t), len(t3)+len(t7))\n",
    "test_eq(len(get_files(path/'train'/'3', extensions='.jpg', recurse=False)),0)\n",
    "test_eq(len(t), len(get_files(path, extensions='.png', recurse=True, folders='train')))\n",
    "t"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#| hide\n",
    "test_eq(len(get_files(path/'train'/'3', recurse=False)),346)\n",
    "test_eq(len(get_files(path, extensions='.png', recurse=True, folders=['train', 'test'])),729)\n",
    "test_eq(len(get_files(path, extensions='.png', recurse=True, folders='train')),709)\n",
    "test_eq(len(get_files(path, extensions='.png', recurse=True, folders='training')),0)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "It's often useful to be able to create functions with customized behavior. `fastai.data` generally uses functions named as CamelCase verbs ending in `er` to create these functions. `FileGetter` is a simple example of such a function creator."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "def FileGetter(suf='', extensions=None, recurse=True, folders=None):\n",
    "    \"Create `get_files` partial function that searches path suffix `suf`, only in `folders`, if specified, and passes along args\"\n",
    "    def _inner(o, extensions=extensions, recurse=recurse, folders=folders):\n",
    "        return get_files(o/suf, extensions, recurse, folders)\n",
    "    return _inner"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "fpng = FileGetter(extensions='.png', recurse=False)\n",
    "test_eq(len(t7), len(fpng(path/'train'/'7')))\n",
    "test_eq(len(t), len(fpng(path/'train', recurse=True)))\n",
    "fpng_r = FileGetter(extensions='.png', recurse=True)\n",
    "test_eq(len(t), len(fpng_r(path/'train')))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "image_extensions = set(k for k,v in mimetypes.types_map.items() if v.startswith('image/'))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "def get_image_files(path, recurse=True, folders=None):\n",
    "    \"Get image files in `path` recursively, only in `folders`, if specified.\"\n",
    "    return get_files(path, extensions=image_extensions, recurse=recurse, folders=folders)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "This is simply `get_files` called with a list of standard image extensions."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "test_eq(len(t), len(get_image_files(path, recurse=True, folders='train')))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "def ImageGetter(suf='', recurse=True, folders=None):\n",
    "    \"Create `get_image_files` partial that searches suffix `suf` and passes along `kwargs`, only in `folders`, if specified\"\n",
    "    def _inner(o, recurse=recurse, folders=folders): return get_image_files(o/suf, recurse, folders)\n",
    "    return _inner"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Same as `FileGetter`, but for image extensions."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "test_eq(len(get_files(path/'train', extensions='.png', recurse=True, folders='3')),\n",
    "        len(ImageGetter(   'train',                    recurse=True, folders='3')(path)))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "def get_text_files(path, recurse=True, folders=None):\n",
    "    \"Get text files in `path` recursively, only in `folders`, if specified.\"\n",
    "    return get_files(path, extensions=['.txt'], recurse=recurse, folders=folders)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "class ItemGetter(ItemTransform):\n",
    "    \"Creates a proper transform that applies `itemgetter(i)` (even on a tuple)\"\n",
    "    _retain = False\n",
    "    def __init__(self, i): self.i = i\n",
    "    def encodes(self, x): return x[self.i]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "test_eq(ItemGetter(1)((1,2,3)),  2)\n",
    "test_eq(ItemGetter(1)(L(1,2,3)), 2)\n",
    "test_eq(ItemGetter(1)([1,2,3]),  2)\n",
    "test_eq(ItemGetter(1)(np.array([1,2,3])),  2)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "class AttrGetter(ItemTransform):\n",
    "    \"Creates a proper transform that applies `attrgetter(nm)` (even on a tuple)\"\n",
    "    _retain = False\n",
    "    def __init__(self, nm, default=None): store_attr()\n",
    "    def encodes(self, x): return getattr(x, self.nm, self.default)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "test_eq(AttrGetter('shape')(torch.randn([4,5])), [4,5])\n",
    "test_eq(AttrGetter('shape', [0])([4,5]), [0])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Split"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The next set of functions are used to *split* data into training and validation sets. The functions return two lists - a list of indices or masks for each of training and validation sets."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "def RandomSplitter(valid_pct=0.2, seed=None):\n",
    "    \"Create function that splits `items` between train/val with `valid_pct` randomly.\"\n",
    "    def _inner(o):\n",
    "        if seed is not None: torch.manual_seed(seed)\n",
    "        rand_idx = L(list(torch.randperm(len(o)).numpy()))\n",
    "        cut = int(valid_pct * len(o))\n",
    "        return rand_idx[cut:],rand_idx[:cut]\n",
    "    return _inner"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def _test_splitter(f, items=None):\n",
    "    \"A basic set of condition a splitter must pass\"\n",
    "    items = ifnone(items, range_of(30))\n",
    "    trn,val = f(items)\n",
    "    assert 0<len(trn)<len(items)\n",
    "    assert all(o not in val for o in trn)\n",
    "    test_eq(len(trn), len(items)-len(val))\n",
    "    # test random seed consistency\n",
    "    test_eq(f(items)[0], trn)\n",
    "    return trn, val"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "((#24) [10,18,16,23,28,26,20,7,21,22...], (#6) [12,0,6,25,8,15])"
      ]
     },
     "execution_count": null,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "_test_splitter(RandomSplitter(seed=42))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Use scikit-learn train_test_split. This allow to *split* items in a stratified fashion (uniformely according to the ‘labels‘ distribution)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "def TrainTestSplitter(test_size=0.2, random_state=None, stratify=None, train_size=None, shuffle=True):\n",
    "    \"Split `items` into random train and test subsets using sklearn train_test_split utility.\"\n",
    "    def _inner(o, **kwargs):\n",
    "        train,valid = train_test_split(range_of(o), test_size=test_size, random_state=random_state,\n",
    "                                        stratify=stratify, train_size=train_size, shuffle=shuffle)\n",
    "        return L(train), L(valid)\n",
    "    return _inner"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "src = list(range(30))\n",
    "labels = [0] * 20 + [1] * 10\n",
    "test_size = 0.2\n",
    "\n",
    "f = TrainTestSplitter(test_size=test_size, random_state=42, stratify=labels)\n",
    "trn,val = _test_splitter(f, items=src)\n",
    "\n",
    "# test labels distribution consistency\n",
    "# there should be test_size % of zeroes and ones respectively in the validation set\n",
    "test_eq(len([t for t in val if t < 20]) / 20, test_size)\n",
    "test_eq(len([t for t in val if t > 20]) / 10, test_size)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "def IndexSplitter(valid_idx):\n",
    "    \"Split `items` so that `val_idx` are in the validation set and the others in the training set\"\n",
    "    def _inner(o):\n",
    "        train_idx = np.setdiff1d(np.array(range_of(o)), np.array(valid_idx))\n",
    "        return L(train_idx, use_list=True), L(valid_idx, use_list=True)\n",
    "    return _inner"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "items = 'a,b,c,d,e,f,g,h,i,j'.split(',')  #to make obvious that splits indexes and not items.\n",
    "splitter = IndexSplitter([3,7,9])\n",
    "\n",
    "_test_splitter(splitter, items)\n",
    "test_eq(splitter(items),[[0,1,2,4,5,6,8],[3,7,9]])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "def EndSplitter(valid_pct=0.2, valid_last=True):\n",
    "    \"Create function that splits `items` between train/val with `valid_pct` at the end if `valid_last` else at the start. Useful for ordered data.\"\n",
    "    assert 0<valid_pct<1, \"valid_pct must be in (0,1)\"\n",
    "    def _inner(o):\n",
    "        idxs = range_of(o)\n",
    "        cut = int(valid_pct * len(o))\n",
    "        return (idxs[:-cut], idxs[-cut:]) if valid_last else (idxs[cut:],idxs[:cut])\n",
    "    return _inner"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "items = range_of(10)\n",
    "\n",
    "splitter_last = EndSplitter(valid_last=True)\n",
    "_test_splitter(splitter_last)\n",
    "test_eq(splitter_last(items), ([0,1,2,3,4,5,6,7], [8,9]))\n",
    "\n",
    "splitter_start = EndSplitter(valid_last=False)\n",
    "_test_splitter(splitter_start)\n",
    "test_eq(splitter_start(items), ([2,3,4,5,6,7,8,9], [0,1]))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "def _grandparent_idxs(items, name):\n",
    "    def _inner(items, name): return mask2idxs(Path(o).parent.parent.name == name for o in items)\n",
    "    return [i for n in L(name) for i in _inner(items,n)]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "def GrandparentSplitter(train_name='train', valid_name='valid'):\n",
    "    \"Split `items` from the grand parent folder names (`train_name` and `valid_name`).\"\n",
    "    def _inner(o):\n",
    "        return _grandparent_idxs(o, train_name),_grandparent_idxs(o, valid_name)\n",
    "    return _inner"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "fnames = [path/'train/3/9932.png', path/'valid/7/7189.png',\n",
    "          path/'valid/7/7320.png', path/'train/7/9833.png',\n",
    "          path/'train/3/7666.png', path/'valid/3/925.png',\n",
    "          path/'train/7/724.png', path/'valid/3/93055.png']\n",
    "splitter = GrandparentSplitter()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "_test_splitter(splitter, items=fnames)\n",
    "test_eq(splitter(fnames),[[0,3,4,6],[1,2,5,7]])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "fnames2 = fnames + [path/'test/3/4256.png', path/'test/7/2345.png', path/'valid/7/6467.png']\n",
    "splitter = GrandparentSplitter(train_name=('train', 'valid'), valid_name='test')\n",
    "_test_splitter(splitter, items=fnames2)\n",
    "test_eq(splitter(fnames2),[[0,3,4,6,1,2,5,7,10],[8,9]])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "def FuncSplitter(func):\n",
    "    \"Split `items` by result of `func` (`True` for validation, `False` for training set).\"\n",
    "    def _inner(o):\n",
    "        val_idx = mask2idxs(func(o_) for o_ in o)\n",
    "        return IndexSplitter(val_idx)(o)\n",
    "    return _inner"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "splitter = FuncSplitter(lambda o: Path(o).parent.parent.name == 'valid')\n",
    "_test_splitter(splitter, fnames)\n",
    "test_eq(splitter(fnames),[[0,3,4,6],[1,2,5,7]])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "def MaskSplitter(mask):\n",
    "    \"Split `items` depending on the value of `mask`.\"\n",
    "    def _inner(o): return IndexSplitter(mask2idxs(mask))(o)\n",
    "    return _inner"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "items = list(range(6))\n",
    "splitter = MaskSplitter([True,False,False,True,False,True])\n",
    "_test_splitter(splitter, items)\n",
    "test_eq(splitter(items),[[1,2,4],[0,3,5]])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "def FileSplitter(fname):\n",
    "    \"Split `items` by providing file `fname` (contains names of valid items separated by newline).\"\n",
    "    valid = Path(fname).read_text().split('\\n')\n",
    "    def _func(x): return x.name in valid\n",
    "    def _inner(o): return FuncSplitter(_func)(o)\n",
    "    return _inner"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "with tempfile.TemporaryDirectory() as d:\n",
    "    fname = Path(d)/'valid.txt'\n",
    "    fname.write_text('\\n'.join([Path(fnames[i]).name for i in [1,3,4]]))\n",
    "    splitter = FileSplitter(fname)\n",
    "    _test_splitter(splitter, fnames)\n",
    "    test_eq(splitter(fnames),[[0,2,5,6,7],[1,3,4]])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "def ColSplitter(col='is_valid', on=None):\n",
    "    \"Split `items` (supposed to be a dataframe) by value in `col`\"\n",
    "    def _inner(o):\n",
    "        assert isinstance(o, pd.DataFrame), \"ColSplitter only works when your items are a pandas DataFrame\"\n",
    "        c = o.iloc[:,col] if isinstance(col, int) else o[col]\n",
    "        if on is None:      valid_idx = c.values.astype('bool')\n",
    "        elif is_listy(on):  valid_idx = c.isin(on)\n",
    "        else:               valid_idx = c == on\n",
    "        return IndexSplitter(mask2idxs(valid_idx))(o)\n",
    "    return _inner"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df = pd.DataFrame({'a': [0,1,2,3,4], 'b': [True,False,True,True,False]})\n",
    "splits = ColSplitter('b')(df)\n",
    "test_eq(splits, [[1,4], [0,2,3]])\n",
    "# Works with strings or index\n",
    "splits = ColSplitter(1)(df)\n",
    "test_eq(splits, [[1,4], [0,2,3]])\n",
    "# does not get confused if the type of 'is_valid' is integer, but it meant to be a yes/no\n",
    "df = pd.DataFrame({'a': [0,1,2,3,4], 'is_valid': [1,0,1,1,0]})\n",
    "splits_by_int = ColSplitter('is_valid')(df)\n",
    "test_eq(splits_by_int, [[1,4], [0,2,3]])\n",
    "# optionally pass a specific value to split on\n",
    "df = pd.DataFrame({'a': [0,1,2,3,4,5], 'b': [1,2,3,1,2,3]})\n",
    "splits_on_val = ColSplitter('b', 3)(df)\n",
    "test_eq(splits_on_val, [[0,1,3,4], [2,5]])\n",
    "# or multiple values\n",
    "splits_on_val = ColSplitter('b', [2,3])(df)\n",
    "test_eq(splits_on_val, [[0,3], [1,2,4,5]])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "def RandomSubsetSplitter(train_sz, valid_sz, seed=None):\n",
    "    \"Take randoms subsets of `splits` with `train_sz` and `valid_sz`\"\n",
    "    assert 0 < train_sz < 1\n",
    "    assert 0 < valid_sz < 1\n",
    "    assert train_sz + valid_sz <= 1.\n",
    "\n",
    "    def _inner(o):\n",
    "        if seed is not None: torch.manual_seed(seed)\n",
    "        train_len,valid_len = int(len(o)*train_sz),int(len(o)*valid_sz)\n",
    "        idxs = L(list(torch.randperm(len(o)).numpy()))\n",
    "        return idxs[:train_len],idxs[train_len:train_len+valid_len]\n",
    "    return _inner"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "items = list(range(100))\n",
    "valid_idx = list(np.arange(70,100))\n",
    "splitter = RandomSubsetSplitter(0.3, 0.1)\n",
    "splits = RandomSubsetSplitter(0.3, 0.1)(items)\n",
    "test_eq(len(splits[0]), 30)\n",
    "test_eq(len(splits[1]), 10)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Label"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The final set of functions is used to *label* a single item of data."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "def parent_label(o):\n",
    "    \"Label `item` with the parent folder name.\"\n",
    "    return Path(o).parent.name"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Note that `parent_label` doesn't have anything customize, so it doesn't return a function - you can just use it directly."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "['3', '7', '7', '7', '3', '3', '7', '3']"
      ]
     },
     "execution_count": null,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "test_eq(parent_label(fnames[0]), '3')\n",
    "test_eq(parent_label(\"fastai_dev/dev/data/mnist_tiny/train/3/9932.png\"), '3')\n",
    "[parent_label(o) for o in fnames]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#| hide\n",
    "#test for MS Windows when os.path.sep is '\\\\' instead of '/'\n",
    "test_eq(parent_label(os.path.join(\"fastai_dev\",\"dev\",\"data\",\"mnist_tiny\",\"train\", \"3\", \"9932.png\") ), '3')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "class RegexLabeller():\n",
    "    \"Label `item` with regex `pat`.\"\n",
    "    def __init__(self, pat, match=False):\n",
    "        self.pat = re.compile(pat)\n",
    "        self.matcher = self.pat.match if match else self.pat.search\n",
    "\n",
    "    def __call__(self, o):\n",
    "        o = str(o).replace(os.sep, posixpath.sep)\n",
    "        res = self.matcher(o)\n",
    "        assert res,f'Failed to find \"{self.pat}\" in \"{o}\"'\n",
    "        return res.group(1)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "`RegexLabeller` is a very flexible function since it handles any regex search of the stringified item. Pass `match=True` to use `re.match` (i.e. check only start of string), or `re.search` otherwise (default).\n",
    "\n",
    "For instance, here's an example the replicates the previous `parent_label` results."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "['3', '7', '7', '7', '3', '3', '7', '3']"
      ]
     },
     "execution_count": null,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "f = RegexLabeller(fr'{posixpath.sep}(\\d){posixpath.sep}')\n",
    "test_eq(f(fnames[0]), '3')\n",
    "[f(o) for o in fnames]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "['3', '7', '7', '7', '3', '3', '7', '3']"
      ]
     },
     "execution_count": null,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "f = RegexLabeller(fr'{posixpath.sep}(\\d){posixpath.sep}')\n",
    "a1 = Path(fnames[0]).as_posix()\n",
    "test_eq(f(a1), '3')\n",
    "[f(o) for o in fnames]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "f = RegexLabeller(r'(\\d*)', match=True)\n",
    "test_eq(f(fnames[0].name), '9932')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "class ColReader(DisplayedTransform):\n",
    "    \"Read `cols` in `row` with potential `pref` and `suff`\"\n",
    "    def __init__(self, cols, pref='', suff='', label_delim=None):\n",
    "        store_attr()\n",
    "        self.pref = str(pref) + os.path.sep if isinstance(pref, Path) else pref\n",
    "        self.cols = L(cols)\n",
    "\n",
    "    def _do_one(self, r, c):\n",
    "        o = r[c] if isinstance(c, int) or not c in getattr(r, '_fields', []) else getattr(r, c)\n",
    "        if len(self.pref)==0 and len(self.suff)==0 and self.label_delim is None: return o\n",
    "        if self.label_delim is None: return f'{self.pref}{o}{self.suff}'\n",
    "        else: return o.split(self.label_delim) if len(o)>0 else []\n",
    "\n",
    "    def __call__(self, o, **kwargs):\n",
    "        if len(self.cols) == 1: return self._do_one(o, self.cols[0])\n",
    "        return L(self._do_one(o, c) for c in self.cols)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "`cols` can be a list of column names or a list of indices (or a mix of both). If `label_delim` is passed, the result is split using it."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df = pd.DataFrame({'a': 'a b c d'.split(), 'b': ['1 2', '0', '', '1 2 3']})\n",
    "f = ColReader('a', pref='0', suff='1')\n",
    "test_eq([f(o) for o in df.itertuples()], '0a1 0b1 0c1 0d1'.split())\n",
    "\n",
    "f = ColReader('b', label_delim=' ')\n",
    "test_eq([f(o) for o in df.itertuples()], [['1', '2'], ['0'], [], ['1', '2', '3']])\n",
    "\n",
    "df['a1'] = df['a']\n",
    "f = ColReader(['a', 'a1'], pref='0', suff='1')\n",
    "test_eq([f(o) for o in df.itertuples()], [L('0a1', '0a1'), L('0b1', '0b1'), L('0c1', '0c1'), L('0d1', '0d1')])\n",
    "\n",
    "df = pd.DataFrame({'a': [L(0,1), L(2,3,4), L(5,6,7)]})\n",
    "f = ColReader('a')\n",
    "test_eq([f(o) for o in df.itertuples()], [L(0,1), L(2,3,4), L(5,6,7)])\n",
    "\n",
    "df['name'] = df['a']\n",
    "f = ColReader('name')\n",
    "test_eq([f(df.iloc[0,:])], [L(0,1)])\n",
    "\n",
    "df['mask'] = df['a']\n",
    "f = ColReader('mask')\n",
    "test_eq([f(o) for o in df.itertuples()], [L(0,1), L(2,3,4), L(5,6,7)])\n",
    "test_eq([f(df.iloc[0,:])], [L(0,1)])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Categorize -"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "class CategoryMap(CollBase):\n",
    "    \"Collection of categories with the reverse mapping in `o2i`\"\n",
    "    def __init__(self, col, sort=True, add_na=False, strict=False):\n",
    "        if hasattr(col, 'dtype') and isinstance(col.dtype, CategoricalDtype):\n",
    "            items = L(col.cat.categories, use_list=True)\n",
    "            #Remove non-used categories while keeping order\n",
    "            if strict: items = L(o for o in items if o in col.unique())\n",
    "        else:\n",
    "            if not hasattr(col,'unique'): col = L(col, use_list=True)\n",
    "            # `o==o` is the generalized definition of non-NaN used by Pandas\n",
    "            items = L(o for o in col.unique() if o==o)\n",
    "            if sort: items = items.sorted()\n",
    "        self.items = '#na#' + items if add_na else items\n",
    "        self.o2i = defaultdict(int, self.items.val2idx()) if add_na else dict(self.items.val2idx())\n",
    "\n",
    "    def map_objs(self,objs):\n",
    "        \"Map `objs` to IDs\"\n",
    "        return L(self.o2i[o] for o in objs)\n",
    "\n",
    "    def map_ids(self,ids):\n",
    "        \"Map `ids` to objects in vocab\"\n",
    "        return L(self.items[o] for o in ids)\n",
    "\n",
    "    def __eq__(self,b): return all_equal(b,self)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "t = CategoryMap([4,2,3,4])\n",
    "test_eq(t, [2,3,4])\n",
    "test_eq(t.o2i, {2:0,3:1,4:2})\n",
    "test_eq(t.map_objs([2,3]), [0,1])\n",
    "test_eq(t.map_ids([0,1]), [2,3])\n",
    "test_fail(lambda: t.o2i['unseen label'])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "t = CategoryMap([4,2,3,4], add_na=True)\n",
    "test_eq(t, ['#na#',2,3,4])\n",
    "test_eq(t.o2i, {'#na#':0,2:1,3:2,4:3})"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "t = CategoryMap(pd.Series([4,2,3,4]), sort=False)\n",
    "test_eq(t, [4,2,3])\n",
    "test_eq(t.o2i, {4:0,2:1,3:2})"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "col = pd.Series(pd.Categorical(['M','H','L','M'], categories=['H','M','L'], ordered=True))\n",
    "t = CategoryMap(col)\n",
    "test_eq(t, ['H','M','L'])\n",
    "test_eq(t.o2i, {'H':0,'M':1,'L':2})"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "col = pd.Series(pd.Categorical(['M','H','M'], categories=['H','M','L'], ordered=True))\n",
    "t = CategoryMap(col, strict=True)\n",
    "test_eq(t, ['H','M'])\n",
    "test_eq(t.o2i, {'H':0,'M':1})"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "class Categorize(DisplayedTransform):\n",
    "    \"Reversible transform of category string to `vocab` id\"\n",
    "    loss_func,order=CrossEntropyLossFlat(),1\n",
    "    def __init__(self, vocab=None, sort=True, add_na=False):\n",
    "        if vocab is not None: vocab = CategoryMap(vocab, sort=sort, add_na=add_na)\n",
    "        store_attr()\n",
    "\n",
    "    def setups(self, dsets):\n",
    "        if self.vocab is None and dsets is not None: self.vocab = CategoryMap(dsets, sort=self.sort, add_na=self.add_na)\n",
    "        self.c = len(self.vocab)\n",
    "\n",
    "    def encodes(self, o):\n",
    "        try:\n",
    "            return TensorCategory(self.vocab.o2i[o])\n",
    "        except KeyError as e:\n",
    "            raise KeyError(f\"Label '{o}' was not included in the training dataset\") from e\n",
    "    def decodes(self, o): return Category      (self.vocab    [o])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "class Category(str, ShowTitle): _show_args = {'label': 'category'}"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "cat = Categorize()\n",
    "tds = Datasets(['cat', 'dog', 'cat'], tfms=[cat])\n",
    "test_eq(cat.vocab, ['cat', 'dog'])\n",
    "test_eq(cat('cat'), 0)\n",
    "test_eq(cat.decode(1), 'dog')\n",
    "test_stdout(lambda: show_at(tds,2), 'cat')\n",
    "test_fail(lambda: cat('bird'))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "cat = Categorize(add_na=True)\n",
    "tds = Datasets(['cat', 'dog', 'cat'], tfms=[cat])\n",
    "test_eq(cat.vocab, ['#na#', 'cat', 'dog'])\n",
    "test_eq(cat('cat'), 1)\n",
    "test_eq(cat.decode(2), 'dog')\n",
    "test_stdout(lambda: show_at(tds,2), 'cat')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "cat = Categorize(vocab=['dog', 'cat'], sort=False, add_na=True)\n",
    "tds = Datasets(['cat', 'dog', 'cat'], tfms=[cat])\n",
    "test_eq(cat.vocab, ['#na#', 'dog', 'cat'])\n",
    "test_eq(cat('dog'), 1)\n",
    "test_eq(cat.decode(2), 'cat')\n",
    "test_stdout(lambda: show_at(tds,2), 'cat')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Multicategorize -"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "class MultiCategorize(Categorize):\n",
    "    \"Reversible transform of multi-category strings to `vocab` id\"\n",
    "    loss_func,order=BCEWithLogitsLossFlat(),1\n",
    "    def __init__(self, vocab=None, add_na=False): super().__init__(vocab=vocab,add_na=add_na,sort=vocab==None)\n",
    "\n",
    "    def setups(self, dsets):\n",
    "        if not dsets: return\n",
    "        if self.vocab is None:\n",
    "            vals = set()\n",
    "            for b in dsets: vals = vals.union(set(b))\n",
    "            self.vocab = CategoryMap(list(vals), add_na=self.add_na)\n",
    "\n",
    "    def encodes(self, o):\n",
    "        if not all(elem in self.vocab.o2i.keys() for elem in o):\n",
    "            diff = [elem for elem in o if elem not in self.vocab.o2i.keys()]\n",
    "            diff_str = \"', '\".join(diff)\n",
    "            raise KeyError(f\"Labels '{diff_str}' were not included in the training dataset\")\n",
    "        return TensorMultiCategory([self.vocab.o2i[o_] for o_ in o])\n",
    "    def decodes(self, o): return MultiCategory      ([self.vocab    [o_] for o_ in o])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "class MultiCategory(L):\n",
    "    def show(self, ctx=None, sep=';', color='black', **kwargs):\n",
    "        return show_title(sep.join(self.map(str)), ctx=ctx, color=color, **kwargs)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "cat = MultiCategorize()\n",
    "tds = Datasets([['b', 'c'], ['a'], ['a', 'c'], []], tfms=[cat])\n",
    "test_eq(tds[3][0], TensorMultiCategory([]))\n",
    "test_eq(cat.vocab, ['a', 'b', 'c'])\n",
    "test_eq(cat(['a', 'c']), tensor([0,2]))\n",
    "test_eq(cat([]), tensor([]))\n",
    "test_eq(cat.decode([1]), ['b'])\n",
    "test_eq(cat.decode([0,2]), ['a', 'c'])\n",
    "test_stdout(lambda: show_at(tds,2), 'a;c')\n",
    "\n",
    "# if vocab supplied, ensure it maintains its order (i.e., it doesn't sort)\n",
    "cat = MultiCategorize(vocab=['z', 'y', 'x'])\n",
    "test_eq(cat.vocab, ['z','y','x'])\n",
    "\n",
    "test_fail(lambda: cat('bird'))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "class OneHotEncode(DisplayedTransform):\n",
    "    \"One-hot encodes targets\"\n",
    "    order=2\n",
    "    def __init__(self, c=None): store_attr()\n",
    "\n",
    "    def setups(self, dsets):\n",
    "        if self.c is None: self.c = len(L(getattr(dsets, 'vocab', None)))\n",
    "        if not self.c: warn(\"Couldn't infer the number of classes, please pass a value for `c` at init\")\n",
    "\n",
    "    def encodes(self, o): return TensorMultiCategory(one_hot(o, self.c).float())\n",
    "    def decodes(self, o): return one_hot_decode(o, None)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Works in conjunction with ` MultiCategorize` or on its own if you have one-hot encoded targets (pass a `vocab` for decoding and `do_encode=False` in this case)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "_tfm = OneHotEncode(c=3)\n",
    "test_eq(_tfm([0,2]), tensor([1.,0,1]))\n",
    "test_eq(_tfm.decode(tensor([0,1,1])), [1,2])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "tds = Datasets([['b', 'c'], ['a'], ['a', 'c'], []], [[MultiCategorize(), OneHotEncode()]])\n",
    "test_eq(tds[1], [tensor([1.,0,0])])\n",
    "test_eq(tds[3], [tensor([0.,0,0])])\n",
    "test_eq(tds.decode([tensor([False, True, True])]), [['b','c']])\n",
    "test_eq(type(tds[1][0]), TensorMultiCategory)\n",
    "test_stdout(lambda: show_at(tds,2), 'a;c')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#| hide\n",
    "#test with passing the vocab\n",
    "tds = Datasets([['b', 'c'], ['a'], ['a', 'c'], []], [[MultiCategorize(vocab=['a', 'b', 'c']), OneHotEncode()]])\n",
    "test_eq(tds[1], [tensor([1.,0,0])])\n",
    "test_eq(tds[3], [tensor([0.,0,0])])\n",
    "test_eq(tds.decode([tensor([False, True, True])]), [['b','c']])\n",
    "test_eq(type(tds[1][0]), TensorMultiCategory)\n",
    "test_stdout(lambda: show_at(tds,2), 'a;c')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "class EncodedMultiCategorize(Categorize):\n",
    "    \"Transform of one-hot encoded multi-category that decodes with `vocab`\"\n",
    "    loss_func,order=BCEWithLogitsLossFlat(),1\n",
    "    def __init__(self, vocab):\n",
    "        super().__init__(vocab, sort=vocab==None)\n",
    "        self.c = len(vocab)\n",
    "    def encodes(self, o): return TensorMultiCategory(tensor(o).float())\n",
    "    def decodes(self, o): return MultiCategory (one_hot_decode(o, self.vocab))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "_tfm = EncodedMultiCategorize(vocab=['a', 'b', 'c'])\n",
    "test_eq(_tfm([1,0,1]), tensor([1., 0., 1.]))\n",
    "test_eq(type(_tfm([1,0,1])), TensorMultiCategory)\n",
    "test_eq(_tfm.decode(tensor([False, True, True])), ['b','c'])\n",
    "\n",
    "_tfm2 = EncodedMultiCategorize(vocab=['c', 'b', 'a'])\n",
    "test_eq(_tfm2.vocab, ['c', 'b', 'a'])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "class RegressionSetup(DisplayedTransform):\n",
    "    \"Transform that floatifies targets\"\n",
    "    loss_func=MSELossFlat()\n",
    "    def __init__(self, c=None): store_attr()\n",
    "\n",
    "    def encodes(self, o): return tensor(o).float()\n",
    "    def decodes(self, o): return TitledFloat(o) if o.ndim==0 else TitledTuple(o_.item() for o_ in o)\n",
    "    def setups(self, dsets):\n",
    "        if self.c is not None: return\n",
    "        try: self.c = len(dsets[0]) if hasattr(dsets[0], '__len__') else 1\n",
    "        except: self.c = 0"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "_tfm = RegressionSetup()\n",
    "dsets = Datasets([0, 1, 2], RegressionSetup)\n",
    "test_eq(dsets.c, 1)\n",
    "test_eq_type(dsets[0], (tensor(0.),))\n",
    "\n",
    "dsets = Datasets([[0, 1, 2], [3,4,5]], RegressionSetup)\n",
    "test_eq(dsets.c, 3)\n",
    "test_eq_type(dsets[0], (tensor([0.,1.,2.]),))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "def get_c(dls):\n",
    "    if getattr(dls, 'c', False): return dls.c\n",
    "    if nested_attr(dls, 'train.after_item.c', False): return dls.train.after_item.c\n",
    "    if nested_attr(dls, 'train.after_batch.c', False): return dls.train.after_batch.c\n",
    "    vocab = getattr(dls, 'vocab', [])\n",
    "    if len(vocab) > 0 and is_listy(vocab[-1]): vocab = vocab[-1]\n",
    "    return len(vocab)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## End-to-end dataset example with MNIST"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Let's show how to use those functions to grab the mnist dataset in a `Datasets`. First we grab all the images."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "path = untar_data(URLs.MNIST_TINY)\n",
    "items = get_image_files(path)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Then we split between train and validation depending on the folder."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "((#3) [Path('/Users/jhoward/.fastai/data/mnist_tiny/train/7/9243.png'),Path('/Users/jhoward/.fastai/data/mnist_tiny/train/7/9519.png'),Path('/Users/jhoward/.fastai/data/mnist_tiny/train/7/7534.png')],\n",
       " (#3) [Path('/Users/jhoward/.fastai/data/mnist_tiny/valid/7/9294.png'),Path('/Users/jhoward/.fastai/data/mnist_tiny/valid/7/9257.png'),Path('/Users/jhoward/.fastai/data/mnist_tiny/valid/7/8175.png')])"
      ]
     },
     "execution_count": null,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "splitter = GrandparentSplitter()\n",
    "splits = splitter(items)\n",
    "train,valid = (items[i] for i in splits)\n",
    "train[:3],valid[:3]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Our inputs are images that we open and convert to tensors, our targets are labeled depending on the parent directory and are categories."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from PIL import Image"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def open_img(fn:Path): return Image.open(fn).copy()\n",
    "def img2tensor(im:Image.Image): return TensorImage(array(im)[None])\n",
    "\n",
    "tfms = [[open_img, img2tensor],\n",
    "        [parent_label, Categorize()]]\n",
    "train_ds = Datasets(train, tfms)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "x,y = train_ds[3]\n",
    "xd,yd = decode_at(train_ds,3)\n",
    "test_eq(parent_label(train[3]),yd)\n",
    "test_eq(array(Image.open(train[3])),xd[0].numpy())"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "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",
      "text/plain": [
       "<Figure size 100x100 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "ax = show_at(train_ds, 3, cmap=\"Greys\", figsize=(1,1))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "assert ax.title.get_text() in ('3','7')\n",
    "test_fig_exists(ax)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## ToTensor -"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "class ToTensor(Transform):\n",
    "    \"Convert item to appropriate tensor class\"\n",
    "    order = 5"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## IntToFloatTensor -"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "class IntToFloatTensor(DisplayedTransform):\n",
    "    \"Transform image to float tensor, optionally dividing by 255 (e.g. for images).\"\n",
    "    order = 10 #Need to run after PIL transforms on the GPU\n",
    "    def __init__(self, div=255., div_mask=1): store_attr()\n",
    "    def encodes(self, o:TensorImage): return o.float().div_(self.div)\n",
    "    def encodes(self, o:TensorMask ): return (o.long() / self.div_mask).long()\n",
    "    def decodes(self, o:TensorImage): return ((o.clamp(0., 1.) * self.div).long()) if self.div else o"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "t = (TensorImage(tensor(1)),tensor(2).long(),TensorMask(tensor(3)))\n",
    "tfm = IntToFloatTensor()\n",
    "ft = tfm(t)\n",
    "test_eq(ft, [1./255, 2, 3])\n",
    "test_eq(type(ft[0]), TensorImage)\n",
    "test_eq(type(ft[2]), TensorMask)\n",
    "test_eq(ft[0].type(),'torch.FloatTensor')\n",
    "test_eq(ft[1].type(),'torch.LongTensor')\n",
    "test_eq(ft[2].type(),'torch.LongTensor')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Normalization -"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "def broadcast_vec(dim, ndim, *t, cuda=True):\n",
    "    \"Make a vector broadcastable over `dim` (out of `ndim` total) by prepending and appending unit axes\"\n",
    "    v = [1]*ndim\n",
    "    v[dim] = -1\n",
    "    f = to_device if cuda else noop\n",
    "    return [f(tensor(o).view(*v)) for o in t]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "@docs\n",
    "class Normalize(DisplayedTransform):\n",
    "    \"Normalize/denorm batch of `TensorImage`\"\n",
    "    parameters,order = L('mean', 'std'),99\n",
    "    def __init__(self, mean=None, std=None, axes=(0,2,3)): store_attr()\n",
    "\n",
    "    @classmethod\n",
    "    def from_stats(cls, mean, std, dim=1, ndim=4, cuda=True): return cls(*broadcast_vec(dim, ndim, mean, std, cuda=cuda))\n",
    "\n",
    "    def setups(self, dl:DataLoader):\n",
    "        if self.mean is None or self.std is None:\n",
    "            x,*_ = dl.one_batch()\n",
    "            self.mean,self.std = x.mean(self.axes, keepdim=True),x.std(self.axes, keepdim=True)+1e-7\n",
    "    def encodes(self, x:TensorImage): return (x-self.mean) / self.std\n",
    "    def decodes(self, x:TensorImage):\n",
    "        f = to_cpu if x.device.type=='cpu' else noop\n",
    "        return (x*f(self.std) + f(self.mean))\n",
    "\n",
    "    _docs=dict(encodes=\"Normalize batch\", decodes=\"Denormalize batch\", setups=\"Calculate mean/std statistics from DataLoader if not provided\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "mean,std = [0.5]*3,[0.5]*3\n",
    "mean,std = broadcast_vec(1, 4, mean, std)\n",
    "batch_tfms = [IntToFloatTensor(), Normalize.from_stats(mean,std)]\n",
    "tdl = TfmdDL(train_ds, after_batch=batch_tfms, bs=4, device=default_device())"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "x,y  = tdl.one_batch()\n",
    "xd,yd = tdl.decode((x,y))\n",
    "\n",
    "assert x.type().endswith('.FloatTensor')\n",
    "test_eq(xd.type(), 'torch.LongTensor')\n",
    "test_eq(type(x), TensorImage)\n",
    "test_eq(type(y), TensorCategory)\n",
    "assert x.mean()<0.0\n",
    "assert x.std()>0.3\n",
    "assert 0<xd.float().mean()/255.<1\n",
    "assert 0<xd.float().std()/255.<0.7"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#| hide\n",
    "nrm = Normalize()\n",
    "batch_tfms = [IntToFloatTensor(), nrm]\n",
    "tdl = TfmdDL(train_ds, after_batch=batch_tfms, bs=4)\n",
    "x,y  = tdl.one_batch()\n",
    "test_close(x.mean(), 0.0, 1e-4)\n",
    "assert x.std()>0.9, x.std()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#Just for visuals\n",
    "from fastai.vision.core import *"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "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",
      "text/plain": [
       "<Figure size 72x72 with 1 Axes>"
      ]
     },
     "metadata": {
      "needs_background": "light"
     },
     "output_type": "display_data"
    },
    {
     "data": {
      "image/png": "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",
      "text/plain": [
       "<Figure size 72x72 with 1 Axes>"
      ]
     },
     "metadata": {
      "needs_background": "light"
     },
     "output_type": "display_data"
    },
    {
     "data": {
      "image/png": "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",
      "text/plain": [
       "<Figure size 72x72 with 1 Axes>"
      ]
     },
     "metadata": {
      "needs_background": "light"
     },
     "output_type": "display_data"
    },
    {
     "data": {
      "image/png": "iVBORw0KGgoAAAANSUhEUgAAAEQAAABUCAYAAAA7xZEpAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjUuMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8qNh9FAAAACXBIWXMAAAsTAAALEwEAmpwYAAAKyElEQVR4nO2b2W9c133HP7+7zZ2d5Az3TZZoiRYtW97q2M7iOqkbp30ymqBtUsAPaZoCRR66/AktWrRPfehLWqCp+5C0LooArRHbMWzUQhq78iLLUiWLIUWJ+6YhZ597zzl9IEWJY0emyxkOm84HmAfeDV9+7rnnnvO794oxhja3sFod4LDRFlJHW0gdbSF1tIXU0RZSR1tIHS0XIiL/KCILIrIpIh+KyDdbmqfVAzMRmQAmjTFVERkHXgd+zRjzdivytLyFGGMuGGOqN//c/h1rVZ6WCwEQkb8RkRJwCVgAXmxZllZfMjcRERt4DHgS+AtjTNCKHIeihQAYY5Qx5gwwBPx+q3IcGiG34fD/tQ8RkR4R+U0RSYiILSK/CvwW8GrLMrWyDxGRbuAF4H62Ts4M8NfGmO+2LNNh6VQPC4exD2kpbSF1tIXU0RZSh3Onlb9iffUXtsd9Rf+zfNzydgupoy2kjraQOtpC6mgLqaMtpI62kDraQuq448CsKYggjouVSiDpFCYaQcc8jACWEMZclG+BATHgbtSw81VkZR2d28DUatDEGfqBC7EiESSZpDYxzOopn1K/IRyoYbka21GcGpjhC11XyCufqnH4weUHUVc7GfiPFIl3Z9E3cuhSqWn5GiZEHAcrnQLHQfzIznLjuZi4j/YdwphLrcOh3GVR6hfKozUSmRLjXev4doBnKR5NT/NIdIqKcakYl2sjXZzzB1guZKimR+j8II09u4TeLGCCWqPi79AwIVYySXhimCDpUs46sD1TqHRaFAcNYTZgcGidJ3qn+FbXGSICvgg2giVbG9sIFha2CFAFqnx26BWCQc2LJ4Z5pzDKS//0GQbe8HCuzKNWVhoVf4d9C7FiMWSon+pwBwufiaBihiCld4SYeJXOTIG+ZJ7THbP8UmKKISeCtcf+PCIWEYEJbx43GfKvQ4+QOx4ju5yAQymkt5v5L/WSOxXy4jN/RcY2uNyaSFrbrQDYOft7lXE7E57DmLvECw9McjY9SupqJ/bk9H7jf4R9CzGuQy0NVjIgYxs6Lf9jt9NoKiZkIVTMqQRXa1mmqz1UtUNgbHJBjI3AJ9QWNe0wkV7gnug8j/pXGXO3+iRXbDxLYdkG5GNn7/tm/0KiHpUeTVdHcVfLqKekA1a04Y3SGC+tTfDe7CByJY5dE6wa+KuG2KrCKWvssuLfPnsX/3KqxB/c/zpjHT/bb8w9s/9LJleg80IHhY0sv+58g0y0xJH4GqGxKYQe69U4C/kkgbKpVl1qGxG8FYfoqhBf0FiBxgoN3kaIm6sgtRBqAZGcT2nD40YYb8T/uWf2LSS8Nkv2+WV6OtKoH/WR68vy6j1HsQLw8ob4oqLvnXkIQ4zWUK6gCsWtnY3edSx924ArfqqHUr/NSi2534ifiv3fdo3BBCGmUMRZ2iBeCcEksEKDXdZ462VMPo9RGpTCBCFo9XMPJ46DeB6FARvG80zE53atL4UuqmIjdzjGfmjMOEQrdLGInt4689H3blv1KQ8l0ShWIs7GuOIvT/+Q05F5IAqAMoaNWhQp2kitOS8HHPxc5hOQvm4qI5043WWOuSskra2OelWVWVIuU1f66H7bwlnZpBlt5NAJqY50snI6wr0D15jwHG5GnAmjvF8dJnPWJvODd1G/6C3E7u6GbAdzD0fwnljj6exFYGv8oozhH9Y+z+szY/Qshpgw/EiH3CgOjRDTnyF/PI392A1ee/Dv8cUBLJQxVE3Iy1fGSZ6JEbu6hgrDpuVonRARxLaxR4aojnQx96hP9f4Sv33X+7jYO8P7kglY1yDXo2TPl5G1XFNjtUyI2DYSiVA+mmHpkQh9X5zlhRPfJyIOrtg72+W1YVElSMwI1hvv0ry2sUXLKmbmgRMsP5Qgd1JzfGKGZ/vfJSLO9tT/FklLGKRA+ck8c8nHGThTwrl07fDXQ/aK2DbiuayfTCBfWeObo+f4k8zF7bX2R7ZPWh4xMfzp6R/y1t1HebnwOP1LaaRSbYqQAy8yW8kkMtRPflR47uibPJW4eOftt0sGJ71Fnk6dx/3KCpe+04O6b6w5+Zpy1Dsg8RhBT5LKQMC3O6Z4KPLJ+1hYjLkRPu/X+NuJ5/nzL3+fzaPRpuQ7+D5EKURp0EJgbo01PwwMl2p9fPf655ia7MNJ1UjEK/zOsbf4jdT7dFkOEXFJSkifs4HyBHE9TBg0tAp/8EKMQUKNKKFqbt0zLtUGeTV3ktmfDjL2ozKFEZ9ST4x/j9/LF+KX8CXYEmIJGVNGeyB+BFNUYBo3iD9wIXozj3MNBl47wsPBH+4sjy5ZxBYNIz+r4E0v07kWJx2PMJMe4rn8c/zRPT/m68kFYuLSZdVYfyhE9L30vLaIamAp8eCFlErocpn0T21i8907y93ZNcLrswBbY43tWX92+FFWTQfvDI/y9eQCrtikLY+xY4tM6n66Pkghk43Ld2BCrGQSqzuD6oxT6YkhK2WcqYWd9fpm0aiO+PUi2k0w9VR21/K0V0aiIcaWOxQuPz0HJyQRJ+hLU+7z2Ry2ydY0ztnlT9zPXssTd21uVHbfVXw7xHY12I0tNjddiJ1KQV83i0/2EH12CWUK2IHDms7Qu4c32k2pjJPzqAS7o1aUgwotUI2d9Ta/hUR9wmyCjbvhpYnnuRjEOVM4wfd6ntrb/kpBEKL17gFLRbmYwEIaXAZovpBUgs2jUVS2hi82GavMaGQVFTPYHWl0uYKpVj9+XxHKD93F+rjHg73nASiZGutKcfGDEXretHDnlhs64Wu6EBPxqHZYuNEAV2xiUiVjF1BRjaRTWGKhbn/FYbssgG0jjkNh0CV/t+JYbOuxZUkr1rWHv2DTcTmP2cg3NG/zhbg2QQJ8f6vk1207+LLG0w+f55U/vo/sWYvuH89gqlVMtYY5MUpxJM76CZvSSMipk9P8bs/7fC46icbl73IP89rycTo/VNhXZtH5/2NCsMBYO8++iYhLxHZ5uuMCK/cnuJAbo+uDTqxygFWusjkSJzdmE9xX5Jmxy3yt6y2e8AMgQmAU720MMT3bzbHlGurGjYbHbf5d5kaRjskk80d3P4F73J9nePhF3nx2jP/60ijaWGgjjPvX6fHy3OPPccRdZ8BWgE9BV8lpzdvnj9J3xsK7ttCUYlHzW0i5gr8aYG26rKgqScsmJh5ZO0rWhge8Kb7dMbVTJVNGo7k5WXO3f7CiDdfDNNEFh9RkHrPZ2EvlJk0XolbXiZyr0dN7nC8f+T2eOXKRP+s9u+f9S6ZGSSu+dfkbXL/Qx+h/VrEvzTTttarmd6pBDbW2TmK+yuqVFG/Gj7CQfQNfhJjYuLJVUFZGY4tF2dQoGUXFGCpGWFRx1lWCmavddP234M9tojY3m5b3wIbu7rkp7l7sZvWxPp785e9wfHiJ5wZ/wri3yIS3LUIrvrdxHy8vnWRyupfInEdqypCYqzG+mMfK5dHrje9Ib+fAhKjcBuQ26MjEKQ7Euaz6eSl2L9cTXeT0NEUTJ6+ivLo8zuTVXhKXPdLTitS5FdSVKTSf/jnx/4Y7fpXZjA+IrGQSK9OJiUdRyQjasdCejRgDBpxigFWsIsUypljCFIroSqXRMX7uB0QHXw/J53cNpix2F3YNNOUh9l5pv9pdR1tIHW0hdbSF1NH+9r+Odgupoy2kjraQOtpC6mgLqaMtpI7/AXlkQLm2EW5OAAAAAElFTkSuQmCC",
      "text/plain": [
       "<Figure size 72x72 with 1 Axes>"
      ]
     },
     "metadata": {
      "needs_background": "light"
     },
     "output_type": "display_data"
    }
   ],
   "source": [
    "tdl.show_batch((x,y))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "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",
      "text/plain": [
       "<Figure size 72x72 with 1 Axes>"
      ]
     },
     "metadata": {
      "needs_background": "light"
     },
     "output_type": "display_data"
    },
    {
     "data": {
      "image/png": "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",
      "text/plain": [
       "<Figure size 72x72 with 1 Axes>"
      ]
     },
     "metadata": {
      "needs_background": "light"
     },
     "output_type": "display_data"
    },
    {
     "data": {
      "image/png": "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",
      "text/plain": [
       "<Figure size 72x72 with 1 Axes>"
      ]
     },
     "metadata": {
      "needs_background": "light"
     },
     "output_type": "display_data"
    },
    {
     "data": {
      "image/png": "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",
      "text/plain": [
       "<Figure size 72x72 with 1 Axes>"
      ]
     },
     "metadata": {
      "needs_background": "light"
     },
     "output_type": "display_data"
    }
   ],
   "source": [
    "#| hide\n",
    "x,y = cast(x,Tensor),cast(y,Tensor) #Lose type of tensors (to emulate predictions)\n",
    "test_ne(type(x), TensorImage)\n",
    "tdl.show_batch((x,y), figsize=(1,1)) #Check that types are put back by dl."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Export -"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#| hide\n",
    "from nbdev import nbdev_export\n",
    "nbdev_export()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "jupytext": {
   "split_at_heading": true
  },
  "kernelspec": {
   "display_name": "python3",
   "language": "python",
   "name": "python3"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
